package com.sdy.dataadapter;

import com.alibaba.druid.filter.logging.Slf4jLogFilter;
import com.alibaba.druid.filter.stat.StatFilter;
import com.alibaba.druid.pool.DruidDataSource;
import com.zaxxer.hikari.HikariDataSource;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;

import javax.sql.DataSource;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 数据源管理
 * @author zhouziqiang 
 */
@Slf4j
public class DataSourceManager {

    private static final Integer DS_EXPIRE_SECOND = 86400;

    private static final Integer DS_EXPIRE_FLUSH_SECOND = 3600;
    
    @Data
    static class DataSourceEx {
        private DataSource dataSource;
        private TransactionTemplate transactionTemplate;
        // 1-new 2-expire
        private int state;
        private long expireTime;
    }
    
    private static final Map<RawDataSource, DataSourceEx> dataSourceMap = new ConcurrentHashMap<>();
    
    private static final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
    
    static {
        executorService.scheduleWithFixedDelay(DataSourceManager::clearExpireDataSource, DS_EXPIRE_FLUSH_SECOND, DS_EXPIRE_FLUSH_SECOND, TimeUnit.SECONDS);
    }

    private static void clearExpireDataSource() {
        Set<Map.Entry<RawDataSource, DataSourceEx>> es = dataSourceMap.entrySet();
        es.forEach(e -> {
            if (e.getValue().getState() == 1) {
                if (e.getValue().getExpireTime() < System.currentTimeMillis()) {
                    e.getValue().setState(2);
                }
            } else {
                synchronized (DataSourceManager.class) {
                    if (e.getValue().getState() == 2) {
                        dataSourceMap.remove(e.getKey());
                        log.info("Clear datasource: {}", e.getValue());
                    }
                }
            }
        });
    }

    public synchronized static DataSourceEx getDataSource(RawDataSource rawDataSource) {
        DataSourceEx dataSourceEx = dataSourceMap.get(rawDataSource);
        if (dataSourceEx == null) {
            if (!dataSourceMap.containsKey(rawDataSource)) {
                dataSourceEx = createDataSourceEx(rawDataSource);
                dataSourceMap.put(rawDataSource, dataSourceEx);
            }
        }
        dataSourceEx.setExpireTime(System.currentTimeMillis() + DS_EXPIRE_SECOND * 1000);
        if (dataSourceEx.getState() == 2) {
            dataSourceEx.setState(1);
            dataSourceMap.putIfAbsent(rawDataSource, dataSourceEx);
        }
        return dataSourceEx;
    }
    
    private static DataSourceEx createDataSourceEx(RawDataSource rawDataSource) {
        DataSource dataSource = createHikariDataSource(rawDataSource);

        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        transactionManager.setNestedTransactionAllowed(false);
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);

        DataSourceEx dataSourceEx = new DataSourceEx();
        dataSourceEx.setDataSource(dataSource);
        dataSourceEx.setTransactionTemplate(transactionTemplate);
        dataSourceEx.setState(1);
        dataSourceEx.setExpireTime(System.currentTimeMillis() + DS_EXPIRE_SECOND * 1000);
        
        log.info("Create new datasource: {}", dataSourceEx);
        return dataSourceEx;
    }
    
    private static DataSource createHikariDataSource(RawDataSource rawDataSource) {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(rawDataSource.getUrl());
        dataSource.setDriverClassName(rawDataSource.getDbDriver().getDriver());
        dataSource.setUsername(rawDataSource.getUsername());
        dataSource.setPassword(rawDataSource.getPassword());
        return dataSource;
    }

    private static DataSource createDruidDataSource(RawDataSource rawDataSource) {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(rawDataSource.getUrl());
        dataSource.setDriverClassName(rawDataSource.getDbDriver().getDriver());
        dataSource.setUsername(rawDataSource.getUsername());
        dataSource.setPassword(rawDataSource.getPassword());
        dataSource.setTestOnBorrow(true);
        dataSource.setTestWhileIdle(true);
        dataSource.setProxyFilters(Arrays.asList(logFilter(), statFilter()));
        return dataSource;
    }

    private static Slf4jLogFilter logFilter() {
        Slf4jLogFilter logFilter = new Slf4jLogFilter();
        logFilter.setStatementLogEnabled(false);
        logFilter.setStatementLogErrorEnabled(false);
        logFilter.setStatementExecutableSqlLogEnable(false);
        logFilter.setStatementCreateAfterLogEnabled(false);
        logFilter.setStatementCloseAfterLogEnabled(false);
        logFilter.setStatementExecuteAfterLogEnabled(false);
        logFilter.setStatementParameterSetLogEnabled(false);
        logFilter.setStatementPrepareAfterLogEnabled(false);
        logFilter.setConnectionConnectBeforeLogEnabled(false);
        logFilter.setConnectionConnectAfterLogEnabled(false);
        logFilter.setResultSetCloseAfterLogEnabled(false);
        return logFilter;
    }

    private static StatFilter statFilter() {
        StatFilter statFilter = new StatFilter();
        statFilter.setLogSlowSql(false);
        statFilter.setSlowSqlMillis(200);
        statFilter.setMergeSql(false);
        return statFilter;
    }
}
